Introduction

  • Keep in mind the tradeoffs based on speed, explainability, simplicity & performance
  • Telco customer data is found on Kaggle
  • Goal is to “Predict behavior to retain customers. You can analyze all relevant customer data and develop focused customer retention programs.”
  • Two ways to think about this problem
    • On an aggregate level understand which customer groups are more likely to churn, improve esisting service &/ develop program geared towards this group (e.g. month-to-month cutomers are more likely to churn, give promotional offer so they can sign up for longer contract)
    • On an individual level, rank order customers by probability of churning, and understand why each customer churns / stays, this is done by building a model & shaply value

Load data

options(scipen=999)
pkgs = c('data.table', 'magrittr', 'caret', 'glmnet', 'ranger', 'plotly', 'iml')
inst = lapply(pkgs, library, character.only=TRUE)
source('~/Documents/R/Utils/functions/summarize.R')
source('~/Documents/R/Utils/functions/model_auc.R')
source('~/Documents/R/Utils/functions/feature_comparison.R')
source('~/Documents/R/Utils/functions/helper.R')
data = fread("~/Documents/R/churn/WA_Fn-UseC_-Telco-Customer-Churn.csv")
seed = 2018
head(data)

EDA

Target distribution

  • month-to month contract are more likely to churn
  • fiber optics customers are more likely to churn
  • no internet service customers are less likely to churn

Variable correlations

  • No internet service from a few variables
data[,.N,.(OnlineSecurity=="No internet service", 
           TechSupport=="No internet service", 
           InternetService=="No", 
           DeviceProtection=="No internet service",
           StreamingMovies=="No internet service", 
           StreamingTV=="No internet service")]

  • age related values: no internet services, etc.
data[, .(Obs = .N, Churn = sum(Churn=="Yes")/.N), .(SeniorCitizen, haveInternet = InternetService!="No")]

  • tenure vs. total charges / montly charges
plot_ly(data, x=~tenure, y=~nMth) 

  • tenure vs. contract
FeatureComparison(list(month2month = data[Contract=="Month-to-month"]$tenure,
                       oneYear = data[Contract=="One year"]$tenure,
                       twoYear = data[Contract=="Two year"]$tenure), "tenure")

Summarize

The goal is to find variables with:

  • missing values
    • Missing in TotalCharges: we can remove rows (11 rows) / use mean tenure / most frequent tenure (1)
  • low variance
  • outliers
sum.data = Summarize(data); sum.data

Feature engineering

Train-test split

data.fe = data[!is.na(TotalCharges)][,-1]
# train-test split
set.seed(seed)
index.test = sample(1:nrow(data.fe), nrow(data.fe)%/%20, replace = F)
data.train = data.fe[-index.test]
data.test = data.fe[index.test]
# check to make sure distribution is consistent
FeatureComparison(list(Train = data.train$Churn, Test = data.test$Churn), 
                  vectorName = "Churn",
                  probability = T)

One-hot-encoding

  • customerID, gender, Partner, Dependents, PhoneService, MultipleLines, InternetService, OnlineSecurity, OnlineBackup, DeviceProtection, TechSupport, StreamingTV, StreamingMovies, Contract, PaperlessBilling, PaymentMethod, Churn
# function
addFeat <- function(d, feature, value) {
  newFeat = paste0(feature, "_", make.names(value))
  d[get(feature) == value, (newFeat) := 1L]
  d[get(feature) != value, (newFeat) := 0L]
  d
}
onehot <- function(d, feature, values = NULL) {
  if (is.null(values)) { 
    values = d[,unique(get(feature))] 
    if (length(values)==2L) values = values[1]
  }
  d = lapply(1:length(values), function(x) addFeat(d, feature, values[x]))[[1]] %>%
    setDT
  d[, (feature) := NULL]
  d
}
# encode
data.oh = copy(data.fe) 
onehot(data.oh, "gender", "male")
onehot(data.oh, "Partner", "Yes")
onehot(data.oh, "Dependents", "Yes")
onehot(data.oh, "PhoneService", "Yes")
onehot(data.oh, "MultipleLines")
onehot(data.oh, "InternetService")
onehot(data.oh, "OnlineSecurity", c("Yes", "No"))
onehot(data.oh, "OnlineBackup", c("Yes", "No"))
onehot(data.oh, "DeviceProtection", c("Yes", "No"))
onehot(data.oh, "TechSupport", c("Yes", "No"))
onehot(data.oh, "StreamingTV", c("Yes", "No"))
onehot(data.oh, "StreamingMovies", c("Yes", "No"))
onehot(data.oh, "Contract")
onehot(data.oh, "PaperlessBilling", "Yes")
onehot(data.oh, "PaymentMethod")
# split
dataOH.train = data.oh[-index.test]
dataOH.test = data.oh[index.test]

Additional work

TBD


Model build

glmnet

  • Consistent results when nfolds >= 10
  • L1 is always better than L2
    • easier for production, keeping track
    • need to make sure distributions of these variables don’t change over time
xlr = model.matrix(Churn ~ ., data = data.fe)[,-1]
ylr = data.fe$Churn
# build models
buildLR <- function(alpha, nfolds, seed = 2018) {
  # alpha = 1 is lasso, alpha = 0 is ridge
  set.seed(seed)
  model = cv.glmnet(xlr[-index.test,], ylr[-index.test], 
                    alpha = alpha, nfolds = nfolds, family = "binomial")
  pred = predict(model, s = model$lambda.min, 
                 newx = xlr[index.test,], type = "response")[,1]
  data.table(alpha = alpha, nfolds = nfolds, lambda = model$lambda.min,
             auc = CalcAUC(scores = pred, target = data.fe[index.test, Churn=="No"]))
}
# check parameter
plr = purrr::map2_dfr(.x = rep(0:1, each = 18), .y = rep(3:20, 2), buildLR) %>%
  setDT %>%
  .[alpha==0, regularization := "L2"] %>%
  .[alpha==1, regularization := "L1"]
plot_ly(plr, x = ~nfolds, y = ~lambda, color = ~regularization, 
        colors = c("red", "blue")) %>% add_markers()

  • A few lambda values are consistently chosen although a lot of values are tested
# build models
set.seed(seed)
mlr.l1 = cv.glmnet(xlr[-index.test,], ylr[-index.test], alpha = 1, nfolds = 10, family = "binomial")
set.seed(seed)
mlr.l2 = cv.glmnet(xlr[-index.test,], ylr[-index.test], alpha = 0, nfolds = 10, family = "binomial")
# check parameter
buildLR2 <- function(alpha, i) {
  model = if (alpha==1) mlr.l1 else mlr.l2
  pred = predict(model, s = model$lambda[i], newx = xlr[index.test,], type = "response")[,1]
  data.table(alpha = alpha, lambda = model$lambda[i],
             auc = CalcAUC(scores = pred, target = data.fe[index.test, Churn=="No"]))
}
plr2 = purrr::map2_dfr(.x = rep(1, length(mlr.l1$lambda)), 
                       .y = 1:length(mlr.l1$lambda), buildLR2) %>%
  rbind(purrr::map2_dfr(.x = rep(0, length(mlr.l2$lambda)), 
                       .y = 1:length(mlr.l2$lambda), buildLR2)) %>%
  setDT %>%
  .[alpha==0, regularization := "L2"] %>%
  .[alpha==1, regularization := "L1"]
# plot
plot_ly(plr2, y = ~auc, x = ~lambda, color = ~regularization, 
        colors = c("red", "blue")) %>% add_markers()

  • Final model – lasso regression has better performance
pred.l1 = predict(mlr.l1, s = mlr.l1$lambda.min, newx = xlr[index.test,], type = "response")[,1]
pred.l2 = predict(mlr.l2, s = mlr.l2$lambda.min, newx = xlr[index.test,], type = "response")[,1]
ModelAUC(scores = data.table(Lasso = pred.l1, Ridge = pred.l2),
         target = data.test[, Churn=="No"])

coef(mlr.l1)
31 x 1 sparse Matrix of class "dgCMatrix"
                                                            1
(Intercept)                          -0.565314600678356260488
genderMale                            .                      
SeniorCitizen                         0.182665785268790237250
PartnerYes                            .                      
DependentsYes                        -0.088101429020788643576
tenure                               -0.030283115549137325229
PhoneServiceYes                      -0.138780310387376565329
MultipleLinesNo phone service         0.000000000000010537242
MultipleLinesYes                      0.086977284729059717305
InternetServiceFiber optic            0.836726328662139917647
InternetServiceNo                    -0.631798942172894673064
OnlineSecurityNo internet service    -0.000018715620585032591
OnlineSecurityYes                    -0.272436842545352719824
OnlineBackupNo internet service      -0.016904375941974417868
OnlineBackupYes                       .                      
DeviceProtectionNo internet service  -0.000000000000010884362
DeviceProtectionYes                   .                      
TechSupportNo internet service       -0.000000000000002202788
TechSupportYes                       -0.215115445968212914973
StreamingTVNo internet service       -0.034367247351094684649
StreamingTVYes                        0.133702318090771349324
StreamingMoviesNo internet service   -0.000014648268145588188
StreamingMoviesYes                    0.148588308399303675733
ContractOne year                     -0.504947348137423257519
ContractTwo year                     -0.924317946353389818803
PaperlessBillingYes                   0.313189272214447589349
PaymentMethodCredit card (automatic)  .                      
PaymentMethodElectronic check         0.374348308394866047255
PaymentMethodMailed check             .                      
MonthlyCharges                        .                      
TotalCharges                          .                      

Logistic Regression (caret)

  • selectionFunction allows to change
    • best, oneSE, tolerance
  • Change the evaluation metrics to AUC
  • We define search values since only 3 values are used by default
  • Caret chose lasso regression as the best model
# 10-fold CV
clr <- trainControl(method = "repeatedcv", number = 10, repeats = 5, 
                    classProbs = TRUE, summaryFunction = twoClassSummary)
# search grid
glr <-  expand.grid(alpha = 0:10/10, lambda = 10^seq(3,-6))
# model                        
set.seed(2018)
mlr <- train(as.factor(Churn) ~ ., data = data.train, 
             method = "glmnet", trControl = clr, tuneGrid = glr, metric = "ROC")
plot(mlr)

mlr$bestTune

RandomForest

  • Gini is better than extratrees
  • Gini at 4 features is consistently better
  • Higher AUC as min node size goes higher, but with diminishing return
  • 10-fold
# input
xrf = copy(data.fe) 
xrf = xrf[, lapply(.SD, as.factor), 
          .SDcols = sum.data[Class=="character" & variable!="customerID", as.character(variable)]] %>%
  cbind(xrf[, mget(sum.data[Class!="character", as.character(variable)])])
# search grid
grf <-  expand.grid(mtry = 1:5*4, 
                    splitrule = c("gini", "extratrees"),
                    min.node.size = 1:5*20)
#                       ntree = c(seq(10, 90, 10), seq(100, 900, 100), seq(1000, 10000, 1000)))
# 10-fold CV
set.seed(seed)
crf10 <- trainControl(method = "repeatedcv", number = 10, repeats = 1, 
                    classProbs = TRUE, summaryFunction = twoClassSummary)
# model                        
set.seed(seed)
mrf <- train(as.factor(Churn) ~ ., data = xrf[-index.test], 
             method = 'ranger', trControl = crf10, tuneGrid = grf, metric = "ROC")
plot(mrf)

mrf$bestTune

RandomForest (one-hot)

# 10-fold CV
set.seed(seed)
crf <- trainControl(method = "repeatedcv", number = 10, repeats = 1, 
                    classProbs = TRUE, summaryFunction = twoClassSummary)
# search grid
grf <-  expand.grid(mtry = 1:5*4, 
                    splitrule = c("gini", "extratrees"),
                    min.node.size = 1:5*20)
# model                        
set.seed(seed)
moh <- train(as.factor(Churn) ~ ., data = dataOH.train, 
             method = 'ranger', trControl = crf, tuneGrid = grf, metric = "ROC")
plot(moh)

moh$bestTune

Performance

  • Ridge regression has worst performance – could be b/c some features are not predictive at all
  • Mixed logistic regression has same performance as lasso – we prefer lasso, since it’s simplier
  • Two RF models have better performance
pred.lr = predict(mlr, newdata = data.test, type = "prob")[,2]
pred.rf = predict(mrf, newdata = data.test, type = "prob")[,2]
pred.oh = predict(moh, newdata = dataOH.test, type = "prob")[,2]
ModelAUC(scores = data.table(Lasso = pred.l1, Ridge = pred.l2, 
                             Mixed = pred.lr, RF = pred.rf, RFOneHot = pred.oh),
         target = data.test[, Churn=="No"])

Model explain

  • iml package
  • We use predicted probability to target individual customers, and use this to build customer retention strategy
    • rank order new customers by the probability of churing, focus on retaining customers who are most likely to churn
    • use important features to target customer groups
    • use shapley to inform us what makes each customer churn

Permutation impact

  • Which features are most / least important?
  • Most important features are InternetService, tenure, Contract, PaperlessBilling
    • We will run PDP to see how each variable affects customer churn
  • We may also try removing the bottom features to get a better model
set.seed(seed)
modelRF = Predictor$new(mrf, data = data.frame(data.train), y = "Churn", type = "prob")
impRF = FeatureImp$new(modelRF, loss = "ce")
plot(impRF) #imp$results


  • One-hot-encoding:
set.seed(seed)
modelOH = Predictor$new(moh, data = data.frame(dataOH.train), y = "Churn", type = "prob")
impOH = FeatureImp$new(modelOH, loss = "ce")
plot(impOH) #imp$results


PDP

  • How does the model use each variable?
  • Be ware of correlation vs. causation
    • e.g. Customer with paperless billing are more likely to churn, doesn’t necessarily mean if we let them sign up for paper billing then they will not churn

Internet Service

  • Fiber optic customers are more likely to churn
pdpRF = FeatureEffect$new(modelRF, method = "pdp", feature = "InternetService")
pdpRF$plot()


pdpOH = FeatureEffect$new(modelOH, method = "pdp", feature = "InternetService_Fiber.optic")
pdpOH$plot()


pdpOH = FeatureEffect$new(modelOH, method = "pdp", feature = "InternetService_No")
pdpOH$plot()


Tenure

pdpRF$set.feature("tenure"); pdpRF$plot()


pdpOH$set.feature("tenure"); pdpOH$plot()


Contract

pdpRF$set.feature("Contract"); pdpRF$plot()


pdpOH$set.feature("Contract_Month.to.month"); pdpOH$plot()


Paperless billing

pdpRF$set.feature("PaperlessBilling"); pdpRF$plot()


Total charges

pdpRF$set.feature("TotalCharges"); pdpRF$plot()


pdpOH$set.feature("TotalCharges"); pdpOH$plot()


Monthly charges

pdpRF$set.feature("MonthlyCharges"); pdpRF$plot()


pdpOH$set.feature("MonthlyCharges"); pdpOH$plot()


Shapley (individual)

  • What makes each customer churn / stay?
  • customer is predicted to churn with a probability of 63.1231189%
i = 100
shapleyRF = Shapley$new(modelRF, x.interest = data.frame(data.test[, -"Churn"][i, ]))
shapleyRF$plot()


  • customer is predicted to churn with a probability of 61.4656818%
i = 100
shapleyOH = Shapley$new(modelOH, x.interest = data.frame(dataOH.test[, -"Churn"][i, ]))
shapleyOH$plot()


Shapley (aggregate)

  • TBD

Conclusion

General

  • Given that the dataset only has ~20 features, we don’t have to build a ML model to inform us what customer group to focus on – e.g. we have identified month-to-month & fiber optics customers are more likely to churn even before making the model. However, building the model allows us to identify customers who are most like to churn and understand on an individual level what makes a customer churn so we can be more proactive.

Explainability

  • Permutation impact (PI) with one-hot-encoded RF model gives more insight in what specific values / groups drive customer churn
  • However, PI results are less stable and varies with different seeds – e.g. monthly charges hurts the normal RF model, but is very important for the RF model with one-hot-encoding
  • Aggregate Shapley seem to be more stable than permutation impact, in giving consistent results and showing most important variables in the decision. It gives the same rank order of top churning reasons with both RF models
  • On the other hand, one-hot-encoding seems to give weird results for shapley, for e.g. customer is predicted to churn b/c of using fiber optics internet, and also b/c of not using DSL – which should be combined together
  • In conclusion, this dataset suggests factorized RF is better than one-hot-encoded RF in both perdicting power and explainability

save(xlr, ylr, plr, plr2, mlr.l1, mlr.l2, mlr, mrf, moh, file = "model.RData")
load("model.RData")
LS0tCnRpdGxlOiAiQ3VzdG9tZXIgQ2h1cm4iCm91dHB1dDogCiAgaHRtbF9ub3RlYm9vazoKICAgIGNvZGVfZm9sZGluZzogaGlkZQogICAgaGlnaGxpZ2h0OiB0YW5nbwogICAgdGhlbWU6IGZsYXRseQogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiB5ZXMKLS0tCgojIyBJbnRyb2R1Y3Rpb24KCiogS2VlcCBpbiBtaW5kIHRoZSB0cmFkZW9mZnMgYmFzZWQgb24gc3BlZWQsIGV4cGxhaW5hYmlsaXR5LCBzaW1wbGljaXR5ICYgcGVyZm9ybWFuY2UKKiBUZWxjbyBjdXN0b21lciBkYXRhIGlzIGZvdW5kIG9uIFtLYWdnbGVdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vYmxhc3RjaGFyL3RlbGNvLWN1c3RvbWVyLWNodXJuKQoqIEdvYWwgaXMgdG8gXyJQcmVkaWN0IGJlaGF2aW9yIHRvIHJldGFpbiBjdXN0b21lcnMuIFlvdSBjYW4gYW5hbHl6ZSBhbGwgcmVsZXZhbnQgY3VzdG9tZXIgZGF0YSBhbmQgZGV2ZWxvcCBmb2N1c2VkIGN1c3RvbWVyIHJldGVudGlvbiBwcm9ncmFtcy4iXwoqIFR3byB3YXlzIHRvIHRoaW5rIGFib3V0IHRoaXMgcHJvYmxlbQogICAgKyBPbiBhbiBhZ2dyZWdhdGUgbGV2ZWwgdW5kZXJzdGFuZCB3aGljaCBjdXN0b21lciBncm91cHMgYXJlIG1vcmUgbGlrZWx5IHRvIGNodXJuLCBpbXByb3ZlIGVzaXN0aW5nIHNlcnZpY2UgJi8gZGV2ZWxvcCBwcm9ncmFtIGdlYXJlZCB0b3dhcmRzIHRoaXMgZ3JvdXAgKGUuZy4gbW9udGgtdG8tbW9udGggY3V0b21lcnMgYXJlIG1vcmUgbGlrZWx5IHRvIGNodXJuLCBnaXZlIHByb21vdGlvbmFsIG9mZmVyIHNvIHRoZXkgY2FuIHNpZ24gdXAgZm9yIGxvbmdlciBjb250cmFjdCkKICAgICsgT24gYW4gaW5kaXZpZHVhbCBsZXZlbCwgcmFuayBvcmRlciBjdXN0b21lcnMgYnkgcHJvYmFiaWxpdHkgb2YgY2h1cm5pbmcsIGFuZCB1bmRlcnN0YW5kIHdoeSBlYWNoIGN1c3RvbWVyIGNodXJucyAvIHN0YXlzLCB0aGlzIGlzIGRvbmUgYnkgYnVpbGRpbmcgYSBtb2RlbCAmIHNoYXBseSB2YWx1ZQoKKioqKioKIyMgTG9hZCBkYXRhCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpvcHRpb25zKHNjaXBlbj05OTkpCnBrZ3MgPSBjKCdkYXRhLnRhYmxlJywgJ21hZ3JpdHRyJywgJ2NhcmV0JywgJ2dsbW5ldCcsICdyYW5nZXInLCAncGxvdGx5JywgJ2ltbCcpCmluc3QgPSBsYXBwbHkocGtncywgbGlicmFyeSwgY2hhcmFjdGVyLm9ubHk9VFJVRSkKc291cmNlKCd+L0RvY3VtZW50cy9SL1V0aWxzL2Z1bmN0aW9ucy9zdW1tYXJpemUuUicpCnNvdXJjZSgnfi9Eb2N1bWVudHMvUi9VdGlscy9mdW5jdGlvbnMvbW9kZWxfYXVjLlInKQpzb3VyY2UoJ34vRG9jdW1lbnRzL1IvVXRpbHMvZnVuY3Rpb25zL2ZlYXR1cmVfY29tcGFyaXNvbi5SJykKc291cmNlKCd+L0RvY3VtZW50cy9SL1V0aWxzL2Z1bmN0aW9ucy9oZWxwZXIuUicpCmRhdGEgPSBmcmVhZCgifi9Eb2N1bWVudHMvUi9jaHVybi9XQV9Gbi1Vc2VDXy1UZWxjby1DdXN0b21lci1DaHVybi5jc3YiKQpzZWVkID0gMjAxOApoZWFkKGRhdGEpCmBgYAoKKioqKioKIyMgRURBCiMjIyBUYXJnZXQgZGlzdHJpYnV0aW9uCgoqIG1vbnRoLXRvIG1vbnRoIGNvbnRyYWN0IGFyZSBtb3JlIGxpa2VseSB0byBjaHVybgoqIGZpYmVyIG9wdGljcyBjdXN0b21lcnMgYXJlIG1vcmUgbGlrZWx5IHRvIGNodXJuCiogbm8gaW50ZXJuZXQgc2VydmljZSBjdXN0b21lcnMgYXJlIGxlc3MgbGlrZWx5IHRvIGNodXJuCgoqKioqKgojIyMgVmFyaWFibGUgY29ycmVsYXRpb25zCgoqIE5vIGludGVybmV0IHNlcnZpY2UgZnJvbSBhIGZldyB2YXJpYWJsZXMgCmBgYHtyfQpkYXRhWywuTiwuKE9ubGluZVNlY3VyaXR5PT0iTm8gaW50ZXJuZXQgc2VydmljZSIsIAogICAgICAgICAgIFRlY2hTdXBwb3J0PT0iTm8gaW50ZXJuZXQgc2VydmljZSIsIAogICAgICAgICAgIEludGVybmV0U2VydmljZT09Ik5vIiwgCiAgICAgICAgICAgRGV2aWNlUHJvdGVjdGlvbj09Ik5vIGludGVybmV0IHNlcnZpY2UiLAogICAgICAgICAgIFN0cmVhbWluZ01vdmllcz09Ik5vIGludGVybmV0IHNlcnZpY2UiLCAKICAgICAgICAgICBTdHJlYW1pbmdUVj09Ik5vIGludGVybmV0IHNlcnZpY2UiKV0KYGBgCgoqKioqKgoKKiBhZ2UgcmVsYXRlZCB2YWx1ZXM6IG5vIGludGVybmV0IHNlcnZpY2VzLCBldGMuCmBgYHtyfQpkYXRhWywgLihPYnMgPSAuTiwgQ2h1cm4gPSBzdW0oQ2h1cm49PSJZZXMiKS8uTiksIAogICAgIC4oU2VuaW9yQ2l0aXplbiwgaGF2ZUludGVybmV0ID0gSW50ZXJuZXRTZXJ2aWNlIT0iTm8iKV0KYGBgCgoqKioqKgoKKiB0ZW51cmUgdnMuIHRvdGFsIGNoYXJnZXMgLyBtb250bHkgY2hhcmdlcyAKYGBge3IsIGZpZy53aWR0aD03LCBmaWcuaGVpZ2h0PTQsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CnBsb3RfbHkoZGF0YSwgeD1+dGVudXJlLCB5PX5Ub3RhbENoYXJnZXMlLyVNb250aGx5Q2hhcmdlcykgCmBgYAoKKioqKioKCiogdGVudXJlIHZzLiBjb250cmFjdApgYGB7ciwgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9NH0KRmVhdHVyZUNvbXBhcmlzb24obGlzdChtb250aDJtb250aCA9IGRhdGFbQ29udHJhY3Q9PSJNb250aC10by1tb250aCJdJHRlbnVyZSwKICAgICAgICAgICAgICAgICAgICAgICBvbmVZZWFyID0gZGF0YVtDb250cmFjdD09Ik9uZSB5ZWFyIl0kdGVudXJlLAogICAgICAgICAgICAgICAgICAgICAgIHR3b1llYXIgPSBkYXRhW0NvbnRyYWN0PT0iVHdvIHllYXIiXSR0ZW51cmUpLCAidGVudXJlIikKYGBgCgoqKioqKgojIyMgU3VtbWFyaXplIAoKVGhlIGdvYWwgaXMgdG8gZmluZCB2YXJpYWJsZXMgd2l0aDogCgoqIG1pc3NpbmcgdmFsdWVzCiAgICArIE1pc3NpbmcgaW4gYFRvdGFsQ2hhcmdlc2A6IHdlIGNhbiByZW1vdmUgcm93cyAoMTEgcm93cykgLyB1c2UgbWVhbiB0ZW51cmUgLyBtb3N0IGZyZXF1ZW50IHRlbnVyZSAoYDFgKQoqIGxvdyB2YXJpYW5jZQoqIG91dGxpZXJzCiAgICAKYGBge3J9CnN1bS5kYXRhID0gU3VtbWFyaXplKGRhdGEpOyBzdW0uZGF0YQpgYGAKCgoqKioqKgojIyBGZWF0dXJlIGVuZ2luZWVyaW5nCiMjIyBUcmFpbi10ZXN0IHNwbGl0CmBgYHtyLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD00fQpkYXRhLmZlID0gZGF0YVshaXMubmEoVG90YWxDaGFyZ2VzKV1bLC0xXQojIHRyYWluLXRlc3Qgc3BsaXQKc2V0LnNlZWQoc2VlZCkKaW5kZXgudGVzdCA9IHNhbXBsZSgxOm5yb3coZGF0YS5mZSksIG5yb3coZGF0YS5mZSklLyUyMCwgcmVwbGFjZSA9IEYpCmRhdGEudHJhaW4gPSBkYXRhLmZlWy1pbmRleC50ZXN0XQpkYXRhLnRlc3QgPSBkYXRhLmZlW2luZGV4LnRlc3RdCiMgY2hlY2sgdG8gbWFrZSBzdXJlIGRpc3RyaWJ1dGlvbiBpcyBjb25zaXN0ZW50CkZlYXR1cmVDb21wYXJpc29uKGxpc3QoVHJhaW4gPSBkYXRhLnRyYWluJENodXJuLCBUZXN0ID0gZGF0YS50ZXN0JENodXJuKSwgCiAgICAgICAgICAgICAgICAgIHZlY3Rvck5hbWUgPSAiQ2h1cm4iLAogICAgICAgICAgICAgICAgICBwcm9iYWJpbGl0eSA9IFQpCmBgYAoKKioqKioKIyMjIE9uZS1ob3QtZW5jb2RpbmcKCiogYHIgc3VtLmRhdGFbQ2xhc3M9PSJjaGFyYWN0ZXIiLCB2YXJpYWJsZV1gCgpgYGB7cn0KIyBmdW5jdGlvbgphZGRGZWF0IDwtIGZ1bmN0aW9uKGQsIGZlYXR1cmUsIHZhbHVlKSB7CiAgbmV3RmVhdCA9IHBhc3RlMChmZWF0dXJlLCAiXyIsIG1ha2UubmFtZXModmFsdWUpKQogIGRbZ2V0KGZlYXR1cmUpID09IHZhbHVlLCAobmV3RmVhdCkgOj0gMUxdCiAgZFtnZXQoZmVhdHVyZSkgIT0gdmFsdWUsIChuZXdGZWF0KSA6PSAwTF0KICBkCn0Kb25laG90IDwtIGZ1bmN0aW9uKGQsIGZlYXR1cmUsIHZhbHVlcyA9IE5VTEwpIHsKICBpZiAoaXMubnVsbCh2YWx1ZXMpKSB7IAogICAgdmFsdWVzID0gZFssdW5pcXVlKGdldChmZWF0dXJlKSldIAogICAgaWYgKGxlbmd0aCh2YWx1ZXMpPT0yTCkgdmFsdWVzID0gdmFsdWVzWzFdCiAgfQogIGQgPSBsYXBwbHkoMTpsZW5ndGgodmFsdWVzKSwgZnVuY3Rpb24oeCkgYWRkRmVhdChkLCBmZWF0dXJlLCB2YWx1ZXNbeF0pKVtbMV1dICU+JQogICAgc2V0RFQKICBkWywgKGZlYXR1cmUpIDo9IE5VTExdCiAgZAp9CiMgZW5jb2RlCmRhdGEub2ggPSBjb3B5KGRhdGEuZmUpIApvbmVob3QoZGF0YS5vaCwgImdlbmRlciIsICJtYWxlIikKb25laG90KGRhdGEub2gsICJQYXJ0bmVyIiwgIlllcyIpCm9uZWhvdChkYXRhLm9oLCAiRGVwZW5kZW50cyIsICJZZXMiKQpvbmVob3QoZGF0YS5vaCwgIlBob25lU2VydmljZSIsICJZZXMiKQpvbmVob3QoZGF0YS5vaCwgIk11bHRpcGxlTGluZXMiKQpvbmVob3QoZGF0YS5vaCwgIkludGVybmV0U2VydmljZSIpCm9uZWhvdChkYXRhLm9oLCAiT25saW5lU2VjdXJpdHkiLCBjKCJZZXMiLCAiTm8iKSkKb25laG90KGRhdGEub2gsICJPbmxpbmVCYWNrdXAiLCBjKCJZZXMiLCAiTm8iKSkKb25laG90KGRhdGEub2gsICJEZXZpY2VQcm90ZWN0aW9uIiwgYygiWWVzIiwgIk5vIikpCm9uZWhvdChkYXRhLm9oLCAiVGVjaFN1cHBvcnQiLCBjKCJZZXMiLCAiTm8iKSkKb25laG90KGRhdGEub2gsICJTdHJlYW1pbmdUViIsIGMoIlllcyIsICJObyIpKQpvbmVob3QoZGF0YS5vaCwgIlN0cmVhbWluZ01vdmllcyIsIGMoIlllcyIsICJObyIpKQpvbmVob3QoZGF0YS5vaCwgIkNvbnRyYWN0IikKb25laG90KGRhdGEub2gsICJQYXBlcmxlc3NCaWxsaW5nIiwgIlllcyIpCm9uZWhvdChkYXRhLm9oLCAiUGF5bWVudE1ldGhvZCIpCiMgc3BsaXQKZGF0YU9ILnRyYWluID0gZGF0YS5vaFstaW5kZXgudGVzdF0KZGF0YU9ILnRlc3QgPSBkYXRhLm9oW2luZGV4LnRlc3RdCmBgYAoKKioqKioKIyMjIEFkZGl0aW9uYWwgd29yawoKVEJECgoqKioqKgojIyBNb2RlbCBidWlsZCB7LnRhYnNldH0KIyMjIGdsbW5ldAoKKiBDb25zaXN0ZW50IHJlc3VsdHMgd2hlbiBuZm9sZHMgPj0gMTAKKiBMMSBpcyBhbHdheXMgYmV0dGVyIHRoYW4gTDIKICAgICsgZWFzaWVyIGZvciBwcm9kdWN0aW9uLCBrZWVwaW5nIHRyYWNrCiAgICArIG5lZWQgdG8gbWFrZSBzdXJlIGRpc3RyaWJ1dGlvbnMgb2YgdGhlc2UgdmFyaWFibGVzIGRvbid0IGNoYW5nZSBvdmVyIHRpbWUKCmBgYHtyLCBmaWcud2lkdGg9OSwgZmlnLmhlaWdodD00fQp4bHIgPSBtb2RlbC5tYXRyaXgoQ2h1cm4gfiAuLCBkYXRhID0gZGF0YS5mZSlbLC0xXQp5bHIgPSBkYXRhLmZlJENodXJuCiMgYnVpbGQgbW9kZWxzCmJ1aWxkTFIgPC0gZnVuY3Rpb24oYWxwaGEsIG5mb2xkcywgc2VlZCA9IDIwMTgpIHsKICAjIGFscGhhID0gMSBpcyBsYXNzbywgYWxwaGEgPSAwIGlzIHJpZGdlCiAgc2V0LnNlZWQoc2VlZCkKICBtb2RlbCA9IGN2LmdsbW5ldCh4bHJbLWluZGV4LnRlc3QsXSwgeWxyWy1pbmRleC50ZXN0XSwgCiAgICAgICAgICAgICAgICAgICAgYWxwaGEgPSBhbHBoYSwgbmZvbGRzID0gbmZvbGRzLCBmYW1pbHkgPSAiYmlub21pYWwiKQogIHByZWQgPSBwcmVkaWN0KG1vZGVsLCBzID0gbW9kZWwkbGFtYmRhLm1pbiwgCiAgICAgICAgICAgICAgICAgbmV3eCA9IHhscltpbmRleC50ZXN0LF0sIHR5cGUgPSAicmVzcG9uc2UiKVssMV0KICBkYXRhLnRhYmxlKGFscGhhID0gYWxwaGEsIG5mb2xkcyA9IG5mb2xkcywgbGFtYmRhID0gbW9kZWwkbGFtYmRhLm1pbiwKICAgICAgICAgICAgIGF1YyA9IENhbGNBVUMoc2NvcmVzID0gcHJlZCwgdGFyZ2V0ID0gZGF0YS5mZVtpbmRleC50ZXN0LCBDaHVybj09Ik5vIl0pKQp9CiMgY2hlY2sgcGFyYW1ldGVyCnBsciA9IHB1cnJyOjptYXAyX2RmcigueCA9IHJlcCgwOjEsIGVhY2ggPSAxOCksIC55ID0gcmVwKDM6MjAsIDIpLCBidWlsZExSKSAlPiUKICBzZXREVCAlPiUKICAuW2FscGhhPT0wLCByZWd1bGFyaXphdGlvbiA6PSAiTDIiXSAlPiUKICAuW2FscGhhPT0xLCByZWd1bGFyaXphdGlvbiA6PSAiTDEiXQpwbG90X2x5KHBsciwgeCA9IH5uZm9sZHMsIHkgPSB+bGFtYmRhLCBjb2xvciA9IH5yZWd1bGFyaXphdGlvbiwgCiAgICAgICAgY29sb3JzID0gYygicmVkIiwgImJsdWUiKSkgJT4lIGFkZF9tYXJrZXJzKCkKYGBgCgoqKioqKgoKKiBBIGZldyBsYW1iZGEgdmFsdWVzIGFyZSBjb25zaXN0ZW50bHkgY2hvc2VuIGFsdGhvdWdoIGEgbG90IG9mIHZhbHVlcyBhcmUgdGVzdGVkCgpgYGB7ciwgZmlnLndpZHRoPTksIGZpZy5oZWlnaHQ9NH0KIyBidWlsZCBtb2RlbHMKc2V0LnNlZWQoc2VlZCkKbWxyLmwxID0gY3YuZ2xtbmV0KHhsclstaW5kZXgudGVzdCxdLCB5bHJbLWluZGV4LnRlc3RdLCBhbHBoYSA9IDEsIG5mb2xkcyA9IDEwLCBmYW1pbHkgPSAiYmlub21pYWwiKQpzZXQuc2VlZChzZWVkKQptbHIubDIgPSBjdi5nbG1uZXQoeGxyWy1pbmRleC50ZXN0LF0sIHlsclstaW5kZXgudGVzdF0sIGFscGhhID0gMCwgbmZvbGRzID0gMTAsIGZhbWlseSA9ICJiaW5vbWlhbCIpCgojIGNoZWNrIHBhcmFtZXRlcgpidWlsZExSMiA8LSBmdW5jdGlvbihhbHBoYSwgaSkgewogIG1vZGVsID0gaWYgKGFscGhhPT0xKSBtbHIubDEgZWxzZSBtbHIubDIKICBwcmVkID0gcHJlZGljdChtb2RlbCwgcyA9IG1vZGVsJGxhbWJkYVtpXSwgbmV3eCA9IHhscltpbmRleC50ZXN0LF0sIHR5cGUgPSAicmVzcG9uc2UiKVssMV0KICBkYXRhLnRhYmxlKGFscGhhID0gYWxwaGEsIGxhbWJkYSA9IG1vZGVsJGxhbWJkYVtpXSwKICAgICAgICAgICAgIGF1YyA9IENhbGNBVUMoc2NvcmVzID0gcHJlZCwgdGFyZ2V0ID0gZGF0YS5mZVtpbmRleC50ZXN0LCBDaHVybj09Ik5vIl0pKQp9CnBscjIgPSBwdXJycjo6bWFwMl9kZnIoLnggPSByZXAoMSwgbGVuZ3RoKG1sci5sMSRsYW1iZGEpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgLnkgPSAxOmxlbmd0aChtbHIubDEkbGFtYmRhKSwgYnVpbGRMUjIpICU+JQogIHJiaW5kKHB1cnJyOjptYXAyX2RmcigueCA9IHJlcCgwLCBsZW5ndGgobWxyLmwyJGxhbWJkYSkpLCAKICAgICAgICAgICAgICAgICAgICAgICAueSA9IDE6bGVuZ3RoKG1sci5sMiRsYW1iZGEpLCBidWlsZExSMikpICU+JQogIHNldERUICU+JQogIC5bYWxwaGE9PTAsIHJlZ3VsYXJpemF0aW9uIDo9ICJMMiJdICU+JQogIC5bYWxwaGE9PTEsIHJlZ3VsYXJpemF0aW9uIDo9ICJMMSJdCiMgcGxvdApwbG90X2x5KHBscjIsIHkgPSB+YXVjLCB4ID0gfmxhbWJkYSwgY29sb3IgPSB+cmVndWxhcml6YXRpb24sIAogICAgICAgIGNvbG9ycyA9IGMoInJlZCIsICJibHVlIikpICU+JSBhZGRfbWFya2VycygpCmBgYAoKKioqKioKCiogRmluYWwgbW9kZWwgLS0gbGFzc28gcmVncmVzc2lvbiBoYXMgYmV0dGVyIHBlcmZvcm1hbmNlCgpgYGB7ciwgZmlnLndpZHRoPTcsIGZpZy5oZWlnaHQ9NH0KcHJlZC5sMSA9IHByZWRpY3QobWxyLmwxLCBzID0gbWxyLmwxJGxhbWJkYS5taW4sIG5ld3ggPSB4bHJbaW5kZXgudGVzdCxdLCB0eXBlID0gInJlc3BvbnNlIilbLDFdCnByZWQubDIgPSBwcmVkaWN0KG1sci5sMiwgcyA9IG1sci5sMiRsYW1iZGEubWluLCBuZXd4ID0geGxyW2luZGV4LnRlc3QsXSwgdHlwZSA9ICJyZXNwb25zZSIpWywxXQpNb2RlbEFVQyhzY29yZXMgPSBkYXRhLnRhYmxlKExhc3NvID0gcHJlZC5sMSwgUmlkZ2UgPSBwcmVkLmwyKSwKICAgICAgICAgdGFyZ2V0ID0gZGF0YS50ZXN0WywgQ2h1cm49PSJObyJdKQpgYGAKCioqKioqCmBgYHtyfQpjb2VmKG1sci5sMSkKYGBgCgoqKioqKgojIyMgTG9naXN0aWMgUmVncmVzc2lvbiAoY2FyZXQpCgoqIGBzZWxlY3Rpb25GdW5jdGlvbmAgYWxsb3dzIHRvIGNoYW5nZSAKICAgICsgYGJlc3RgLCBgb25lU0VgLCBgdG9sZXJhbmNlYAoqIENoYW5nZSB0aGUgZXZhbHVhdGlvbiBtZXRyaWNzIHRvIEFVQwoqIFdlIGRlZmluZSBzZWFyY2ggdmFsdWVzIHNpbmNlIG9ubHkgMyB2YWx1ZXMgYXJlIHVzZWQgYnkgZGVmYXVsdAoqIENhcmV0IGNob3NlIGxhc3NvIHJlZ3Jlc3Npb24gYXMgdGhlIGJlc3QgbW9kZWwKCmBgYHtyfQojIDEwLWZvbGQgQ1YKc2V0LnNlZWQoc2VlZCkKY2xyIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIG51bWJlciA9IDEwLCByZXBlYXRzID0gNSwgCiAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icyA9IFRSVUUsIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSkKIyBzZWFyY2ggZ3JpZApnbHIgPC0gIGV4cGFuZC5ncmlkKGFscGhhID0gMDoxMC8xMCwgbGFtYmRhID0gMTBec2VxKDMsLTYpKQojIG1vZGVsICAgICAgICAgICAgICAgICAgICAgICAgCnNldC5zZWVkKHNlZWQpCm1sciA8LSB0cmFpbihhcy5mYWN0b3IoQ2h1cm4pIH4gLiwgZGF0YSA9IGRhdGEudHJhaW4sIAogICAgICAgICAgICAgbWV0aG9kID0gImdsbW5ldCIsIHRyQ29udHJvbCA9IGNsciwgdHVuZUdyaWQgPSBnbHIsIG1ldHJpYyA9ICJST0MiKQpwbG90KG1scikKYGBgCgpgYGB7cn0KbWxyJGJlc3RUdW5lCmBgYAoKKioqKioKIyMjIFJhbmRvbUZvcmVzdCAKCiogR2luaSBpcyBiZXR0ZXIgdGhhbiBleHRyYXRyZWVzCiogR2luaSBhdCA0IGZlYXR1cmVzIGlzIGNvbnNpc3RlbnRseSBiZXR0ZXIKKiBIaWdoZXIgQVVDIGFzIG1pbiBub2RlIHNpemUgZ29lcyBoaWdoZXIsIGJ1dCB3aXRoIGRpbWluaXNoaW5nIHJldHVybgoqIDEwLWZvbGQKCmBgYHtyfQojIGlucHV0CnhyZiA9IGNvcHkoZGF0YS5mZSkgCnhyZiA9IHhyZlssIGxhcHBseSguU0QsIGFzLmZhY3RvciksIAogICAgICAgICAgLlNEY29scyA9IHN1bS5kYXRhW0NsYXNzPT0iY2hhcmFjdGVyIiAmIHZhcmlhYmxlIT0iY3VzdG9tZXJJRCIsIGFzLmNoYXJhY3Rlcih2YXJpYWJsZSldXSAlPiUKICBjYmluZCh4cmZbLCBtZ2V0KHN1bS5kYXRhW0NsYXNzIT0iY2hhcmFjdGVyIiwgYXMuY2hhcmFjdGVyKHZhcmlhYmxlKV0pXSkKIyBzZWFyY2ggZ3JpZApncmYgPC0gIGV4cGFuZC5ncmlkKG10cnkgPSAxOjUqNCwgCiAgICAgICAgICAgICAgICAgICAgc3BsaXRydWxlID0gYygiZ2luaSIsICJleHRyYXRyZWVzIiksCiAgICAgICAgICAgICAgICAgICAgbWluLm5vZGUuc2l6ZSA9IDE6NSoyMCkKIyAgICAgICAgICAgICAgICAgICAgICAgbnRyZWUgPSBjKHNlcSgxMCwgOTAsIDEwKSwgc2VxKDEwMCwgOTAwLCAxMDApLCBzZXEoMTAwMCwgMTAwMDAsIDEwMDApKSkKIyAxMC1mb2xkIENWCnNldC5zZWVkKHNlZWQpCmNyZjEwIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAicmVwZWF0ZWRjdiIsIG51bWJlciA9IDEwLCByZXBlYXRzID0gMSwgCiAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icyA9IFRSVUUsIHN1bW1hcnlGdW5jdGlvbiA9IHR3b0NsYXNzU3VtbWFyeSkKIyBtb2RlbCAgICAgICAgICAgICAgICAgICAgICAgIApzZXQuc2VlZChzZWVkKQptcmYgPC0gdHJhaW4oYXMuZmFjdG9yKENodXJuKSB+IC4sIGRhdGEgPSB4cmZbLWluZGV4LnRlc3RdLCAKICAgICAgICAgICAgIG1ldGhvZCA9ICdyYW5nZXInLCB0ckNvbnRyb2wgPSBjcmYxMCwgdHVuZUdyaWQgPSBncmYsIG1ldHJpYyA9ICJST0MiKQpwbG90KG1yZikKYGBgCgpgYGB7cn0KbXJmJGJlc3RUdW5lCmBgYAoKKioqKioKIyMjIFJhbmRvbUZvcmVzdCAob25lLWhvdCkKYGBge3J9CiMgMTAtZm9sZCBDVgpzZXQuc2VlZChzZWVkKQpjcmYgPC0gdHJhaW5Db250cm9sKG1ldGhvZCA9ICJyZXBlYXRlZGN2IiwgbnVtYmVyID0gMTAsIHJlcGVhdHMgPSAxLCAKICAgICAgICAgICAgICAgICAgICBjbGFzc1Byb2JzID0gVFJVRSwgc3VtbWFyeUZ1bmN0aW9uID0gdHdvQ2xhc3NTdW1tYXJ5KQojIHNlYXJjaCBncmlkCmdyZiA8LSAgZXhwYW5kLmdyaWQobXRyeSA9IDE6NSo0LCAKICAgICAgICAgICAgICAgICAgICBzcGxpdHJ1bGUgPSBjKCJnaW5pIiwgImV4dHJhdHJlZXMiKSwKICAgICAgICAgICAgICAgICAgICBtaW4ubm9kZS5zaXplID0gMTo1KjIwKQojIG1vZGVsICAgICAgICAgICAgICAgICAgICAgICAgCnNldC5zZWVkKHNlZWQpCm1vaCA8LSB0cmFpbihhcy5mYWN0b3IoQ2h1cm4pIH4gLiwgZGF0YSA9IGRhdGFPSC50cmFpbiwgCiAgICAgICAgICAgICBtZXRob2QgPSAncmFuZ2VyJywgdHJDb250cm9sID0gY3JmLCB0dW5lR3JpZCA9IGdyZiwgbWV0cmljID0gIlJPQyIpCnBsb3QobW9oKQpgYGAKCmBgYHtyfQptb2gkYmVzdFR1bmUKYGBgCgoqKioqKgojIyMgUGVyZm9ybWFuY2UKCiogUmlkZ2UgcmVncmVzc2lvbiBoYXMgd29yc3QgcGVyZm9ybWFuY2UgLS0gY291bGQgYmUgYi9jIHNvbWUgZmVhdHVyZXMgYXJlIG5vdCBwcmVkaWN0aXZlIGF0IGFsbAoqIE1peGVkIGxvZ2lzdGljIHJlZ3Jlc3Npb24gaGFzIHNhbWUgcGVyZm9ybWFuY2UgYXMgbGFzc28gLS0gd2UgcHJlZmVyIGxhc3NvLCBzaW5jZSBpdCdzIHNpbXBsaWVyCiogVHdvIFJGIG1vZGVscyBoYXZlIGJldHRlciBwZXJmb3JtYW5jZSAKCmBgYHtyLCBmaWcud2lkdGg9NywgZmlnLmhlaWdodD00fQpwcmVkLmxyID0gcHJlZGljdChtbHIsIG5ld2RhdGEgPSBkYXRhLnRlc3QsIHR5cGUgPSAicHJvYiIpWywyXQpwcmVkLnJmID0gcHJlZGljdChtcmYsIG5ld2RhdGEgPSBkYXRhLnRlc3QsIHR5cGUgPSAicHJvYiIpWywyXQpwcmVkLm9oID0gcHJlZGljdChtb2gsIG5ld2RhdGEgPSBkYXRhT0gudGVzdCwgdHlwZSA9ICJwcm9iIilbLDJdCk1vZGVsQVVDKHNjb3JlcyA9IGRhdGEudGFibGUoTGFzc28gPSBwcmVkLmwxLCBSaWRnZSA9IHByZWQubDIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIE1peGVkID0gcHJlZC5sciwgUkYgPSBwcmVkLnJmLCBSRk9uZUhvdCA9IHByZWQub2gpLAogICAgICAgICB0YXJnZXQgPSBkYXRhLnRlc3RbLCBDaHVybj09Ik5vIl0pCmBgYAoKKioqKioKIyMgTW9kZWwgZXhwbGFpbiB7LnRhYnNldH0KCiogW2ltbCBwYWNrYWdlXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvaW1sL3ZpZ25ldHRlcy9pbnRyby5odG1sKQoqIFdlIHVzZSBwcmVkaWN0ZWQgcHJvYmFiaWxpdHkgdG8gdGFyZ2V0IGluZGl2aWR1YWwgY3VzdG9tZXJzLCBhbmQgdXNlIHRoaXMgdG8gYnVpbGQgY3VzdG9tZXIgcmV0ZW50aW9uIHN0cmF0ZWd5CiAgICArIHJhbmsgb3JkZXIgbmV3IGN1c3RvbWVycyBieSB0aGUgcHJvYmFiaWxpdHkgb2YgY2h1cmluZywgZm9jdXMgb24gcmV0YWluaW5nIGN1c3RvbWVycyB3aG8gYXJlIG1vc3QgbGlrZWx5IHRvIGNodXJuCiAgICArIHVzZSBpbXBvcnRhbnQgZmVhdHVyZXMgdG8gdGFyZ2V0IGN1c3RvbWVyIGdyb3VwcwogICAgKyB1c2Ugc2hhcGxleSB0byBpbmZvcm0gdXMgd2hhdCBtYWtlcyBlYWNoIGN1c3RvbWVyIGNodXJuCgojIyMgUGVybXV0YXRpb24gaW1wYWN0CgoqIFdoaWNoIGZlYXR1cmVzIGFyZSBtb3N0IC8gbGVhc3QgaW1wb3J0YW50PwoqIE1vc3QgaW1wb3J0YW50IGZlYXR1cmVzIGFyZSBgciBpbXBSRiRyZXN1bHRzWzE6NCwgMV1gCiAgICArIFdlIHdpbGwgcnVuIFBEUCB0byBzZWUgaG93IGVhY2ggdmFyaWFibGUgYWZmZWN0cyBjdXN0b21lciBjaHVybgoqIFdlIG1heSBhbHNvIHRyeSByZW1vdmluZyB0aGUgYm90dG9tIGZlYXR1cmVzIHRvIGdldCBhIGJldHRlciBtb2RlbApgYGB7cn0Kc2V0LnNlZWQoc2VlZCkKbW9kZWxSRiA9IFByZWRpY3RvciRuZXcobXJmLCBkYXRhID0gZGF0YS5mcmFtZShkYXRhLnRyYWluKSwgeSA9ICJDaHVybiIsIHR5cGUgPSAicHJvYiIpCmltcFJGID0gRmVhdHVyZUltcCRuZXcobW9kZWxSRiwgbG9zcyA9ICJjZSIpCnBsb3QoaW1wUkYpICNpbXAkcmVzdWx0cwpgYGAKCioqKioqCgoqIE9uZS1ob3QtZW5jb2Rpbmc6IApgYGB7cn0Kc2V0LnNlZWQoc2VlZCkKbW9kZWxPSCA9IFByZWRpY3RvciRuZXcobW9oLCBkYXRhID0gZGF0YS5mcmFtZShkYXRhT0gudHJhaW4pLCB5ID0gIkNodXJuIiwgdHlwZSA9ICJwcm9iIikKaW1wT0ggPSBGZWF0dXJlSW1wJG5ldyhtb2RlbE9ILCBsb3NzID0gImNlIikKcGxvdChpbXBPSCkgI2ltcCRyZXN1bHRzCmBgYAoKKioqKioKIyMjIFBEUCB7LnRhYnNldH0KCiogSG93IGRvZXMgdGhlIG1vZGVsIHVzZSBlYWNoIHZhcmlhYmxlPwoqIEJlIHdhcmUgb2YgY29ycmVsYXRpb24gdnMuIGNhdXNhdGlvbgogICAgKyBlLmcuIEN1c3RvbWVyIHdpdGggcGFwZXJsZXNzIGJpbGxpbmcgYXJlIG1vcmUgbGlrZWx5IHRvIGNodXJuLCBkb2Vzbid0IG5lY2Vzc2FyaWx5IG1lYW4gaWYgd2UgbGV0IHRoZW0gc2lnbiB1cCBmb3IgcGFwZXIgYmlsbGluZyB0aGVuIHRoZXkgd2lsbCBub3QgY2h1cm4KCiMjIyMgSW50ZXJuZXQgU2VydmljZQoKKiBGaWJlciBvcHRpYyBjdXN0b21lcnMgYXJlIG1vcmUgbGlrZWx5IHRvIGNodXJuCgpgYGB7cn0KcGRwUkYgPSBGZWF0dXJlRWZmZWN0JG5ldyhtb2RlbFJGLCBtZXRob2QgPSAicGRwIiwgZmVhdHVyZSA9ICJJbnRlcm5ldFNlcnZpY2UiKQpwZHBSRiRwbG90KCkKYGBgCgoqKioqKgpgYGB7cn0KcGRwT0ggPSBGZWF0dXJlRWZmZWN0JG5ldyhtb2RlbE9ILCBtZXRob2QgPSAicGRwIiwgZmVhdHVyZSA9ICJJbnRlcm5ldFNlcnZpY2VfRmliZXIub3B0aWMiKQpwZHBPSCRwbG90KCkKYGBgCgoqKioqKgpgYGB7cn0KcGRwT0ggPSBGZWF0dXJlRWZmZWN0JG5ldyhtb2RlbE9ILCBtZXRob2QgPSAicGRwIiwgZmVhdHVyZSA9ICJJbnRlcm5ldFNlcnZpY2VfTm8iKQpwZHBPSCRwbG90KCkKYGBgCgoqKioqKgojIyMjIFRlbnVyZQpgYGB7cn0KcGRwUkYkc2V0LmZlYXR1cmUoInRlbnVyZSIpOyBwZHBSRiRwbG90KCkKYGBgCgoqKioqKgpgYGB7cn0KcGRwT0gkc2V0LmZlYXR1cmUoInRlbnVyZSIpOyBwZHBPSCRwbG90KCkKYGBgCgoqKioqKgojIyMjIENvbnRyYWN0CmBgYHtyfQpwZHBSRiRzZXQuZmVhdHVyZSgiQ29udHJhY3QiKTsgcGRwUkYkcGxvdCgpCmBgYAoKKioqKioKYGBge3J9CnBkcE9IJHNldC5mZWF0dXJlKCJDb250cmFjdF9Nb250aC50by5tb250aCIpOyBwZHBPSCRwbG90KCkKYGBgCgoqKioqKgojIyMjIFBhcGVybGVzcyBiaWxsaW5nCmBgYHtyfQpwZHBSRiRzZXQuZmVhdHVyZSgiUGFwZXJsZXNzQmlsbGluZyIpOyBwZHBSRiRwbG90KCkKYGBgCgoqKioqKgojIyMjIFRvdGFsIGNoYXJnZXMKYGBge3J9CnBkcFJGJHNldC5mZWF0dXJlKCJUb3RhbENoYXJnZXMiKTsgcGRwUkYkcGxvdCgpCmBgYAoKKioqKioKYGBge3J9CnBkcE9IJHNldC5mZWF0dXJlKCJUb3RhbENoYXJnZXMiKTsgcGRwT0gkcGxvdCgpCmBgYAoKKioqKioKIyMjIyBNb250aGx5IGNoYXJnZXMKYGBge3J9CnBkcFJGJHNldC5mZWF0dXJlKCJNb250aGx5Q2hhcmdlcyIpOyBwZHBSRiRwbG90KCkKYGBgCgoqKioqKgpgYGB7cn0KcGRwT0gkc2V0LmZlYXR1cmUoIk1vbnRobHlDaGFyZ2VzIik7IHBkcE9IJHBsb3QoKQpgYGAKCioqKioqCiMjIyBTaGFwbGV5IChpbmRpdmlkdWFsKQoKKiBXaGF0IG1ha2VzIGVhY2ggY3VzdG9tZXIgY2h1cm4gLyBzdGF5PwoqIGN1c3RvbWVyIGlzIHByZWRpY3RlZCB0byBjaHVybiB3aXRoIGEgcHJvYmFiaWxpdHkgb2YgYHIgbW9kZWxSRiRwcmVkaWN0KGRhdGEudGVzdFsxMDBdKVtbMl1dKjEwMGAlCgpgYGB7cn0KaSA9IDEwMApzaGFwbGV5UkYgPSBTaGFwbGV5JG5ldyhtb2RlbFJGLCB4LmludGVyZXN0ID0gZGF0YS5mcmFtZShkYXRhLnRlc3RbLCAtIkNodXJuIl1baSwgXSkpCnNoYXBsZXlSRiRwbG90KCkKYGBgCgoqKioqKgoqIGN1c3RvbWVyIGlzIHByZWRpY3RlZCB0byBjaHVybiB3aXRoIGEgcHJvYmFiaWxpdHkgb2YgYHIgbW9kZWxPSCRwcmVkaWN0KGRhdGFPSC50ZXN0WzEwMF0pW1syXV0qMTAwYCUKCmBgYHtyfQppID0gMTAwCnNoYXBsZXlPSCA9IFNoYXBsZXkkbmV3KG1vZGVsT0gsIHguaW50ZXJlc3QgPSBkYXRhLmZyYW1lKGRhdGFPSC50ZXN0WywgLSJDaHVybiJdW2ksIF0pKQpzaGFwbGV5T0gkcGxvdCgpCmBgYAoKKioqKioKIyMjIFNoYXBsZXkgKGFnZ3JlZ2F0ZSkKCiogVEJECmBgYHtyfQoKYGBgCgoqKioqKgojIyBDb25jbHVzaW9uCiMjIyMgR2VuZXJhbAoKKiBHaXZlbiB0aGF0IHRoZSBkYXRhc2V0IG9ubHkgaGFzIH4yMCBmZWF0dXJlcywgd2UgZG9uJ3QgaGF2ZSB0byBidWlsZCBhIE1MIG1vZGVsIHRvIGluZm9ybSB1cyB3aGF0IGN1c3RvbWVyIGdyb3VwIHRvIGZvY3VzIG9uIC0tIGUuZy4gd2UgaGF2ZSBpZGVudGlmaWVkIG1vbnRoLXRvLW1vbnRoICYgZmliZXIgb3B0aWNzIGN1c3RvbWVycyBhcmUgbW9yZSBsaWtlbHkgdG8gY2h1cm4gZXZlbiBiZWZvcmUgbWFraW5nIHRoZSBtb2RlbC4gSG93ZXZlciwgYnVpbGRpbmcgdGhlIG1vZGVsIGFsbG93cyB1cyB0byBpZGVudGlmeSBjdXN0b21lcnMgd2hvIGFyZSBtb3N0IGxpa2UgdG8gY2h1cm4gYW5kIHVuZGVyc3RhbmQgb24gYW4gaW5kaXZpZHVhbCBsZXZlbCB3aGF0IG1ha2VzIGEgY3VzdG9tZXIgY2h1cm4gc28gd2UgY2FuIGJlIG1vcmUgcHJvYWN0aXZlLgoKIyMjIyBFeHBsYWluYWJpbGl0eSAKCiogUGVybXV0YXRpb24gaW1wYWN0IChQSSkgd2l0aCBvbmUtaG90LWVuY29kZWQgUkYgbW9kZWwgZ2l2ZXMgbW9yZSBpbnNpZ2h0IGluIHdoYXQgc3BlY2lmaWMgdmFsdWVzIC8gZ3JvdXBzIGRyaXZlIGN1c3RvbWVyIGNodXJuCiogSG93ZXZlciwgUEkgcmVzdWx0cyBhcmUgbGVzcyBzdGFibGUgYW5kIHZhcmllcyB3aXRoIGRpZmZlcmVudCBzZWVkcyAtLSBlLmcuIG1vbnRobHkgY2hhcmdlcyBodXJ0cyB0aGUgbm9ybWFsIFJGIG1vZGVsLCBidXQgaXMgdmVyeSBpbXBvcnRhbnQgZm9yIHRoZSBSRiBtb2RlbCB3aXRoIG9uZS1ob3QtZW5jb2RpbmcKKiBBZ2dyZWdhdGUgU2hhcGxleSBzZWVtIHRvIGJlIG1vcmUgc3RhYmxlIHRoYW4gcGVybXV0YXRpb24gaW1wYWN0LCBpbiBnaXZpbmcgY29uc2lzdGVudCByZXN1bHRzIGFuZCBzaG93aW5nIG1vc3QgaW1wb3J0YW50IHZhcmlhYmxlcyBpbiB0aGUgZGVjaXNpb24uIEl0IGdpdmVzIHRoZSBzYW1lIHJhbmsgb3JkZXIgb2YgdG9wIGNodXJuaW5nIHJlYXNvbnMgd2l0aCBib3RoIFJGIG1vZGVscwoqIE9uIHRoZSBvdGhlciBoYW5kLCBvbmUtaG90LWVuY29kaW5nIHNlZW1zIHRvIGdpdmUgd2VpcmQgcmVzdWx0cyBmb3Igc2hhcGxleSwgZm9yIGUuZy4gY3VzdG9tZXIgaXMgcHJlZGljdGVkIHRvIGNodXJuIGIvYyBvZiB1c2luZyBmaWJlciBvcHRpY3MgaW50ZXJuZXQsIGFuZCBhbHNvIGIvYyBvZiBub3QgdXNpbmcgRFNMIC0tIHdoaWNoIHNob3VsZCBiZSBjb21iaW5lZCB0b2dldGhlcgoqIEluIGNvbmNsdXNpb24sIHRoaXMgZGF0YXNldCBzdWdnZXN0cyBmYWN0b3JpemVkIFJGIGlzIGJldHRlciB0aGFuIG9uZS1ob3QtZW5jb2RlZCBSRiBpbiBib3RoIHBlcmRpY3RpbmcgcG93ZXIgYW5kIGV4cGxhaW5hYmlsaXR5CgoqKioqKgpgYGB7ciwgZXZhbD1GQUxTRX0Kc2F2ZSh4bHIsIHlsciwgcGxyLCBwbHIyLCBtbHIubDEsIG1sci5sMiwgbWxyLCBtcmYsIG1vaCwgZmlsZSA9ICJtb2RlbC5SRGF0YSIpCmxvYWQoIm1vZGVsLlJEYXRhIikKYGBgCg==